Skip to content

refactor(core): Protocol composes Dispatcher and StreamDriver; dispatch() public#2057

Draft
felixweinberger wants to merge 3 commits into
fweinberger/r1-dispatcher-driverfrom
fweinberger/r2-protocol-compose
Draft

refactor(core): Protocol composes Dispatcher and StreamDriver; dispatch() public#2057
felixweinberger wants to merge 3 commits into
fweinberger/r1-dispatcher-driverfrom
fweinberger/r2-protocol-compose

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor


Protocol now composes a private Dispatcher (registry/middleware/dispatch) and a private StreamDriver (per-connection state). protocol.ts 1011→393 lines. dispatch(req, env?): AsyncIterable<JSONRPCMessage> is the new public method; _onrequest becomes a thin caller. Types (BaseContext, RequestEnv, RequestOptions) move to context.ts.

Motivation and Context

dispatch() is the seam every later S/F PR uses (handleHttp, tasksPlugin, RequiresInput, ContinuationCompat). Separating it from connection state means the per-request HTTP path holds no shared id-keyed maps.

How Has This Been Tested?

protocol.test.ts is unchanged and passes — proof of behavior parity for the public surface. Conformance server 40/40. Full pnpm test:all passes.

Breaking Changes

None to public API. Types BaseContext/RequestOptions/etc. now re-export from context.ts (same import path).

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

R2 of decomposition; depends on R0, R1. The heavy review of the stack — verify each public Protocol method delegates with the same semantics. Review guide: https://gist.github.com/felixweinberger/5a48e0f14d5aced39ed6a91b61940711.


@felixweinberger felixweinberger added the v2-stateless 2026-06 SDK: Protocol decomposition + SEP alignment (request-first / stateless) label May 12, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

⚠️ No Changeset found

Latest commit: 071f495

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 12, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2057

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2057

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2057

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2057

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2057

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2057

commit: 071f495

@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

1 similar comment
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread packages/core/src/shared/protocol.ts
Comment thread packages/core/src/shared/streamDriver.ts
Comment on lines +363 to +379
this.assertCapabilityForMethod(request.method as RequestMethod);
} catch (error) {
return Promise.reject(error);
}
}
return this._driver.request(request, resultSchema, options);
}

/**
* Emits a notification, which is a one-way message that does not expect a response.
*/
async notification(notification: Notification, options?: NotificationOptions): Promise<void> {
const driver = this._driver;
if (!driver) {
throw new SdkError(SdkErrorCode.NotConnected, 'Not connected');
}
this.assertNotificationCapability(notification.method as NotificationMethod);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit: The as RequestMethod and as NotificationMethod casts here are new in this PR and unnecessary — assertCapabilityForMethod / assertNotificationCapability are declared to take RequestMethod | string / NotificationMethod | string, and request.method / notification.method are already string. The pre-refactor code passed these uncast; drop both assertions to avoid unsafely narrowing a free-form string (custom methods are valid) to the spec literal union.

Extended reasoning...

What

In Protocol._requestWithSchema (protocol.ts:363) the refactored code calls:

this.assertCapabilityForMethod(request.method as RequestMethod);

and in Protocol.notification (protocol.ts:379):

this.assertNotificationCapability(notification.method as NotificationMethod);

Both abstract methods are declared (protocol.ts:113, :120) as:

protected abstract assertCapabilityForMethod(method: RequestMethod | string): void;
protected abstract assertNotificationCapability(method: NotificationMethod | string): void;

Since RequestMethod and NotificationMethod are string-literal unions, RequestMethod | string and NotificationMethod | string both collapse to string. Meanwhile Request.method and Notification.method are typed as plain string (spec.types.ts). The casts therefore add nothing for compilation and unsafely assert that an arbitrary method string is a member of the spec literal union — which is false for custom (non-spec) methods that this API explicitly supports.

Step-by-step

  1. request: Request arrives with request.method: string — e.g. a custom method 'acme/search'.
  2. The PR casts it to RequestMethod, claiming it is one of 'ping' | 'tools/list' | .... That claim is untrue.
  3. assertCapabilityForMethod's parameter type is RequestMethod | string ≡ string, so the call would have type-checked without the cast.
  4. Result: the cast is a pure narrowing lie with zero compilation benefit.

Regression vs. pre-refactor

The diff's removed lines show the previous implementation called this.assertCapabilityForMethod(request.method) and this.assertNotificationCapability(notification.method) with no cast and compiled fine. This PR's stated goal is behavior parity for the public surface; introducing two unjustified as assertions is a small code-quality regression. REVIEW.md's checklist explicitly calls out "Type safety: no unjustified any, no unsafe as assertions".

Why it's worth fixing despite zero runtime impact

One verifier argued this is too trivial to report because the parameter immediately re-widens to string, so no type hole is exploited. That's true for runtime — there is no behavioral bug. But the concern isn't a runtime hole; it's that (a) the cast is dead weight, (b) it sets a misleading precedent that request.method is a RequestMethod at this point (it isn't — custom methods flow through here), and (c) it's a regression vs. the code being replaced. In a parity-focused refactor PR, gratuitous new as casts are exactly the kind of drift worth catching. The fix is a two-token deletion, so the cost/benefit clearly favors mentioning it as a nit.

Fix

this.assertCapabilityForMethod(request.method);
// ...
this.assertNotificationCapability(notification.method);

Comment on lines 152 to 168
/**
* Emits a notification, which is a one-way message that does not expect a response.
* Registers a {@linkcode DispatchMiddleware} around the inbound request path.
* Registration order is outer-to-inner: the first `use()` call wraps all later ones.
*/
async notification(notification: Notification, options?: NotificationOptions): Promise<void> {
if (!this._transport) {
throw new SdkError(SdkErrorCode.NotConnected, 'Not connected');
}

this.assertNotificationCapability(notification.method);

const jsonrpcNotification: JSONRPCNotification = { jsonrpc: '2.0', ...notification };

const debouncedMethods = this._options?.debouncedNotificationMethods ?? [];
// A notification can only be debounced if it's in the list AND it's "simple"
// (i.e., has no parameters and no related request ID that could be lost).
const canDebounce = debouncedMethods.includes(notification.method) && !notification.params && !options?.relatedRequestId;

if (canDebounce) {
// If a notification of this type is already scheduled, do nothing.
if (this._pendingDebouncedNotifications.has(notification.method)) {
return;
}

// Mark this notification type as pending.
this._pendingDebouncedNotifications.add(notification.method);

// Schedule the actual send to happen in the next microtask.
// This allows all synchronous calls in the current event loop tick to be coalesced.
Promise.resolve().then(() => {
// Un-mark the notification so the next one can be scheduled.
this._pendingDebouncedNotifications.delete(notification.method);

// SAFETY CHECK: If the connection was closed while this was pending, abort.
if (!this._transport) {
return;
}

// Send the notification, but don't await it here to avoid blocking.
// Handle potential errors with a .catch().
this._transport?.send(jsonrpcNotification, options).catch(error => this._onerror(error));
});

// Return immediately.
return;
}
use(mw: DispatchMiddleware): this {
this._dispatcher.use(mw);
return this;
}

await this._transport.send(jsonrpcNotification, options);
/**
* Dispatch one inbound request through the middleware chain and registered handler,
* yielding any handler-emitted notifications then exactly one terminal response.
* Transport-free entry point; `env` carries per-request input from the caller.
*/
dispatch(request: JSONRPCRequest, env?: RequestEnv): AsyncGenerator<DispatchOutput, void, void> {
return this._dispatcher.dispatch(request, env);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 use() and dispatch() are un-tagged public methods (inherited onto Client/Server), but their signature types — DispatchMiddleware, DispatchOutput, RequestEnv — are all tagged @internal. Nothing breaks (this repo doesn't strip @internal), but the intent is inconsistent: if these are SDK-internal seams for handleHttp/tasksPlugin as the description says, tag use()/dispatch() @internal too; if they're for end users, drop @internal from the referenced types and add them to core/public deliberately.

Extended reasoning...

What's inconsistent

Protocol.use(mw: DispatchMiddleware) (protocol.ts:156) and Protocol.dispatch(req, env?: RequestEnv): AsyncGenerator<DispatchOutput> (protocol.ts:166) are new methods with full user-facing JSDoc and no @internal tag. The PR title literally says "dispatch() public". But every type in their signatures carries @internal:

  • DispatchMiddleware — dispatcher.ts:46 @internal
  • DispatchOutput — dispatcher.ts:27 @internal
  • DispatchFn — dispatcher.ts:36 @internal
  • RequestEnv — context.ts:236 @internal
  • the Dispatcher class itself — dispatcher.ts:88 @internal

So we have public-visibility methods whose parameter and return types are documented as not-for-external-use.

Why it surfaces to end users (addressing the "Protocol isn't public anyway" objection)

It's true that packages/core/src/exports/public/index.ts:41 deliberately omits the Protocol class from the curated public surface, and @modelcontextprotocol/core is private: true. But that doesn't fully neutralise this: Client extends Protocol<ClientContext> (client.ts:221) and Server extends Protocol<ServerContext> are the end-user-facing classes, and they inherit use() / dispatch(). A consumer of @modelcontextprotocol/client will see client.use(...) and client.dispatch(...) in IntelliSense with full JSDoc, and can call them — the curated core/public re-export list doesn't gate inherited instance methods. So the question "is this meant for end users?" is live regardless of whether Protocol itself is re-exported.

What does not break (addressing the over-stated harm)

The repo has no stripInternal in any tsconfig, no api-extractor config, and typedoc.config.mjs does not set excludeInternal (default false). @internal here is a documentation convention, not a build directive. The types are also runtime-exported via the core barrel (index.tsexport * from './shared/dispatcher.js'; protocol.tsexport * from './context.js'). So generated .d.ts will not reference a missing symbol, and TypeDoc will not produce a dangling link. This is why it's a nit, not a bug.

Concrete walk-through

  1. End user installs @modelcontextprotocol/server and writes const s = new Server(...).
  2. Autocomplete on s. shows use and dispatch with the JSDoc "Registers a DispatchMiddleware around the inbound request path" / "Transport-free entry point".
  3. User goes to write s.use(next => ...) — the parameter type is DispatchMiddleware, whose own JSDoc says @internal. Same for for await (const out of s.dispatch(req))out: DispatchOutput is @internal.
  4. Nothing fails — but the user can't tell whether this is supported API or an implementation detail they shouldn't touch. Meanwhile the PR description says the actual consumers are handleHttp, tasksPlugin, RequiresInput, ContinuationCompat — all in-monorepo.

Suggested resolution

Pick one direction and make the annotations agree:

  • If SDK-internal seam (matches the PR description's stated consumers): add @internal to the JSDoc on use() and dispatch() so they match their parameter types. The "public" in the PR title then just means "public class-member visibility for sibling packages", which is fine.
  • If end-user API: drop @internal from DispatchMiddleware / DispatchOutput / DispatchFn / RequestEnv and add them to packages/core/src/exports/public/index.ts deliberately, per CLAUDE.md § Public API Exports / REVIEW.md "Every new export is intentional".

Either is a one-line doc change; the point is to state intent so later PRs in the stack don't have to guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent is now clearer (DispatchMiddleware added to core/public "referenced by public use() method"; typedoc config notes RequestEnv "stay[s] private"), but the @internal tags were not updated to match, so the inconsistency this comment flagged is still there:

  • DispatchMiddleware (dispatcher.ts:45) is now in core/public and still tagged @internal — those directly contradict. Its constituents DispatchFnDispatchOutput/RequestEnv are also still @internal and not exported, so a user who imports the now-public DispatchMiddleware still lands on @internal types one hop in.
  • dispatch() is still un-tagged while its param/return types (RequestEnv, DispatchOutput) are, per the typedoc comment, intentionally staying private. If that is the decision, add @internal to dispatch()'s JSDoc so it agrees.

Net: drop @internal from DispatchMiddleware (and ideally DispatchFn/DispatchOutput since they are its public shape), and either tag dispatch() @internal or export RequestEnv/DispatchOutput alongside DispatchMiddleware.

Comment on lines +1 to +7
/**
* The {@linkcode Protocol} class composes a stateless {@linkcode Dispatcher}
* (handler registry + dispatch) and a per-connection {@linkcode StreamDriver}
* (id correlation, timeouts, progress) under the same public surface as before.
* Handler-context types live in `./context.ts` and are re-exported here for
* backward-compatible import paths.
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit: CLAUDE.md's "Message Flow" section (lines ~157–166) still describes Protocol._onresponse(), Protocol._onrequest(), Protocol.connect() routing to _onrequest/_onresponse/_onnotification, and the _requestHandlers map on Protocol — those private members no longer exist on Protocol after this PR (they live in StreamDriver and Dispatcher). The new file-header in protocol.ts:1-7 documents the composition correctly; CLAUDE.md should be updated to match so contributors aren't pointed at deleted methods.

Extended reasoning...

What

CLAUDE.md is the repo-level contributor architecture doc (and the file Claude itself loads for repo context). Its Architecture / Message Flow section currently reads:

  • Line 157: "4. Protocol._onresponse() resolves the promise when response arrives"
  • Line 164: "2. Protocol.connect() routes to _onrequest(), _onresponse(), or _onnotification()"
  • Lines 165–166: "3. Protocol._onrequest(): Looks up handler in _requestHandlers map"

After this PR, none of those private members exist on Protocol. _onrequest / _onresponse / _onnotification and the message-routing in onmessage now live in StreamDriver (packages/core/src/shared/streamDriver.ts); _requestHandlers lives in Dispatcher (packages/core/src/shared/dispatcher.ts); and Protocol.connect() no longer routes messages itself — it constructs a StreamDriver and calls driver.start().

Why this PR

R1 (1916e69) only added Dispatcher/StreamDriver alongside the old code. R2 (this PR, 62d993f) is the commit that actually removes _onrequest/_onresponse/_onnotification/_requestHandlers from Protocol and rewires connect(). So this is the PR that turns CLAUDE.md's prose from accurate into stale. The PR adds a correct file-header at protocol.ts:1-7 describing the new composition, but CLAUDE.md was not updated to match — it's not in the changed-files list.

Why it matters

REVIEW.md's checklist says: "Bugfix or behavior change: check whether docs/**/*.md describes the old behavior and needs updating; flag prose that now contradicts the implementation." CLAUDE.md is at the repo root rather than under docs/, but it's the primary contributor-facing architecture doc and is loaded into Claude's context for this repo, so the same principle applies. A new contributor following the Message Flow section will grep for Protocol._onrequest and find nothing.

Step-by-step proof

  1. grep -n "_onrequest\|_onresponse\|_requestHandlers" packages/core/src/shared/protocol.ts → no hits after this PR (protocol.ts is 393 lines, none of these identifiers appear).
  2. grep -n "_onrequest\|_onresponse\|_onnotification" packages/core/src/shared/streamDriver.ts → all three present (private methods on StreamDriver).
  3. grep -n "_requestHandlers" packages/core/src/shared/dispatcher.tsprotected _requestHandlers: Map<...> on Dispatcher.
  4. sed -n '155,170p' CLAUDE.md → still references Protocol._onresponse(), Protocol._onrequest(), _requestHandlers map on Protocol.
  5. PR changed-files list: CLAUDE.md is absent.

Impact

No runtime effect — purely doc drift on internal architecture. Hence nit.

Fix

Update CLAUDE.md's Message Flow section to reflect the new composition, e.g.:

  • Outbound: Protocol.request()StreamDriver.request() (assigns id, registers response handler, sets timeout) → Transport.send(); StreamDriver._onresponse() resolves the promise.
  • Inbound: Transport.onmessageStreamDriver routes to _onrequest() / _onresponse() / _onnotification(); _onrequest() calls Dispatcher.dispatch(), which looks up the handler in Dispatcher._requestHandlers and yields notifications + a terminal response that StreamDriver writes back to the pipe.

Fine to land in this PR or as a follow-up in the same stack.

@felixweinberger felixweinberger force-pushed the fweinberger/r1-dispatcher-driver branch from 1916e69 to 22ba1b0 Compare May 12, 2026 14:38
@felixweinberger felixweinberger force-pushed the fweinberger/r2-protocol-compose branch from 62d993f to 435a2dc Compare May 12, 2026 14:38
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both 🔴 issues are fixed in 435a2dcbuildContext now falls back to { request: env.httpReq, authInfo: env.authInfo } for direct dispatch() callers, and the driver-internal cancelled/progress microtasks have .catch(_onerror). The remaining 🟡 nits are non-blocking; leaving the parity verification of this core-Protocol restructure to a human per the PR's own "heavy review of the stack" note.

Extended reasoning...

Overview

This PR restructures packages/core/src/shared/protocol.ts (1011→393 lines) so Protocol composes a private Dispatcher (handler registry, middleware, dispatch()) and a per-connection StreamDriver (id correlation, timeouts, progress, debounce). Handler-context types (BaseContext, ServerContext, RequestOptions, ProtocolOptions, mergeCapabilities, etc.) move to context.ts and are re-exported from protocol.ts for import-path stability. New public surface on Protocol (and therefore on Client/Server): use(mw) and dispatch(req, env?). streamDriver.ts drops the onresponse tap / sendRaw / removeProgressHandler hooks and adds .catch guards on the driver-internal notification microtasks.

What changed since my last pass

Commit 435a2dc addresses both 🔴 findings: (1) the Dispatcher buildContext closure now synthesizes MessageExtraInfo from env.httpReq/env.authInfo when _transportExtra is absent, so direct server.dispatch(req, { httpReq, authInfo }) callers get ctx.http.req populated; (2) the notifications/cancelled and notifications/progress microtask chains in StreamDriver._onnotification now end in .catch(e => this._onerror(...)), restoring parity with the pre-decomposition error path for throwing onprogress callbacks. The three 🟡 nits (gratuitous as RequestMethod/as NotificationMethod casts, @internal consistency on use()/dispatch() vs. their parameter types, and CLAUDE.md Message-Flow doc drift) were not touched and remain open as non-blockers.

Security risks

None identified. No auth, crypto, or permission logic is changed; authInfo is passed through unchanged. The new dispatch() entry point doesn't bypass any validation that the transport path performs — handler lookup, schema parsing, and _wrapHandler all run identically.

Level of scrutiny

High. This is core SDK protocol framing — every Client/Server request, response, notification, timeout, cancellation, and progress callback flows through the rewritten code. The PR description itself flags it as "the heavy review of the stack" and asks reviewers to "verify each public Protocol method delegates with the same semantics." protocol.test.ts passing unchanged is good evidence but not a substitute for line-by-line parity review of request()/notification()/connect()/close() semantics, and the new use()/dispatch() public methods are an API-surface addition that REVIEW.md says should be deliberate.

Other factors

There is also a small behavioral diff worth a human eye: StreamDriver._onrequest now sends the terminal response without { relatedRequestId: request.id } (only notifications carry it), whereas pre-refactor Protocol._onrequest did not pass relatedRequestId on the response either — so this matches old behavior, but the diff hunk reads as a change because R1's StreamDriver did pass it. Net: parity with pre-decomposition is preserved; just calling it out so it isn't mistaken for a regression. Given scope and the explicit ask for human verification, deferring rather than approving.

…ch() public

Protocol now holds a private Dispatcher (handler registry, middleware) and constructs a StreamDriver per connect(). Public methods delegate; protocol.ts shrinks from ~1011 to ~393 LOC.

dispatch(req, env): AsyncIterable<JSONRPCMessage> is now a public method: any per-request driver (HTTP, FaaS) can iterate it directly without a transport.

Behavior unchanged: protocol.test.ts is unmodified, conformance 40/40.
@felixweinberger felixweinberger force-pushed the fweinberger/r1-dispatcher-driver branch from 22ba1b0 to 6aa731b Compare May 12, 2026 15:56
@felixweinberger felixweinberger force-pushed the fweinberger/r2-protocol-compose branch from 435a2dc to 071f495 Compare May 12, 2026 15:56
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both 🔴 issues are fixed (the buildContext fallback for env.httpReq/authInfo and the .catch on the driver microtask chains) — thanks. The remaining inline notes are nits. I'm not auto-approving only because this is the core Protocol decomposition and you flagged it yourself as the heavy review of the stack; a maintainer should give the delegation parity a once-over.

Extended reasoning...

Overview

Protocol (1011 lines) is decomposed into a thin facade (393 lines) that composes a private Dispatcher (handler registry, middleware, dispatch() async-generator) and a per-connection StreamDriver (id correlation, timeouts, progress, debounce). Handler-context types and mergeCapabilities move to context.ts and are re-exported from protocol.ts for import-path stability. New public surface: Protocol.use(mw) and Protocol.dispatch(req, env?); DispatchMiddleware is added to the curated core/public export list. typedoc.config.mjs gains externalSymbolLinkMappings stubs so treatWarningsAsErrors still passes with the new @internal-tagged types referenced from public JSDoc.

What changed since my last pass

Commit 17aef09 applied both 🔴 fixes exactly as suggested:

  • protocol.ts constructor now synthesizes MessageExtraInfo from env.httpReq/env.authInfo when _transportExtra is absent, so direct dispatch() callers get ctx.http.req populated.
  • streamDriver.ts _onnotification() now appends .catch(e => this._onerror(...)) to both the cancelled and progress microtask chains, restoring parity with the pre-decomposition error path.

Commit 071f495 adds the typedoc link mappings. The bug-hunting pass on the updated revision found nothing new.

Security risks

None identified. This is an internal refactor of in-process message routing; no auth, crypto, network, or input-validation surface changes. authInfo continues to be threaded through unchanged.

Level of scrutiny

High. Protocol is the base class of both Client and Server; every request, response, notification, timeout, and cancellation in the SDK flows through the code being moved here. The PR description itself calls this "the heavy review of the stack" and asks reviewers to "verify each public Protocol method delegates with the same semantics." protocol.test.ts passing unchanged and conformance 40/40 are good signals, but a maintainer should eyeball the delegation seams (fallbackRequestHandler now an accessor pair, setRequestHandler/setNotificationHandler forwarding via type-erased casts, connect() building env.send/env.notify closures that route back through this._requestWithSchema/this.notification, the relatedRequestId no longer being attached to the terminal response send in StreamDriver._onrequest).

Other factors

  • Three open 🟡 nits remain inline (gratuitous as RequestMethod casts; @internal-tag consistency on dispatch()'s param/return types — partially addressed by exporting DispatchMiddleware and the typedoc stubs; stale CLAUDE.md "Message Flow" prose). None block.
  • New public API (use(), dispatch(), DispatchMiddleware export) is intentional per the PR description and the stack's downstream consumers, but per REVIEW.md "Every new export is intentional" a maintainer should sign off on the surface addition.
  • No changeset; fine for a refactor in a stacked branch.

@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2-stateless 2026-06 SDK: Protocol decomposition + SEP alignment (request-first / stateless)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant